ELBの挙動とCloudWatchメトリクスの読み方を徹底的に理解する
よく訓練されたアップル信者、都元です。ELBはAWSにおけるWebシステムを構築する場合、ほぼ確実に利用するコンポーネントとして不動の地位を確立しつつあります。
利用方法としては、ELBを作成して配下にWebサーバを配置するだけというお手軽さがあり、非常に利用しやすいのも大きなメリットです。しかし、ELBの詳細な挙動について、しっかり理解できているでしょうか。本エントリではいつも利用しているELBについて、ちょっと深く突っ込んでみました。
ELBのロードバランシング戦略
ELBの配下には複数のAZにまたがるようにインスタンスを配置するのが一般的です。(cf. AWSにおける可用性の考え方)
ELBを作成すると、DNS名が付与されますが、クライアントがELBにアクセスする際、まずこのホスト名をIPアドレスに変換するDNSの正引きリクエスト(下図中の緑色の矢印)を行います。digコマンドを使って、ELBのドメイン名を正引きしてみると、以下のようになりました。
$ dig ****-1234567890.ap-northeast-1.elb.amazonaws.com (略) ;; ANSWER SECTION: ****-1234567890.ap-northeast-1.elb.amazonaws.com. 20 IN A 198.51.100.236 ****-1234567890.ap-northeast-1.elb.amazonaws.com. 20 IN A 203.0.113.74 (略)
ELB配下のインスタンスが2つのAZにまたがっていた場合、このように、DNS正引きの応答には2つ(以上)のIPアドレスが含まれます。これらのIPアドレスはそれぞれ別のAZに属しています。このことから、ELBはサービスとしてMulti-AZの冗長構成を提供していることがわかります。
このようなDNSの応答に対して、クライアントはどちらかのIPアドレスをランダムに採用します。この時点で、リクエストがどのAZで処理されるかが決まります *1。
DNSラウンドロビンによるAZ振り分けの後、ELBは配下のインスタンスのレイテンシ統計に基いて振り分け先のインスタンスを決定します。
ELBには「各AZに等数のEC2インスタンスを配置すべき」という指針がありますが、その理由は以上のようなアーキテクチャであることが理由です。極端な話、AZ-Aに10台、AZ-Bに1台というインスタンス配分にした場合、まず最初のDNSラウンドロビンで50%-50%で負荷分散され、AZ-Aのインスタンスは1台あたり5%の負荷を受けることになる一方、AZ-Bでは1台で50%全ての負荷を受けることになります。つまりAZ-BのインスタンスはAZ-Aのインスタンスの10倍の負荷が掛かる、ということです。
ELBがリクエストを受けた後、各メトリクスはどのようにカウントされるのか
ELBはリクエスト数や、各レスポンスコード毎の数をCloudWatchから確認できます。
安定している環境でしたら、時間帯別リクエスト数を確認したり、意図しない400系や500系のHTTPレスポンスが発生していないかどうかチェックするような用途が中心かと思います。しかし、大規模サイトやスパイクアクセスを受けるようなシステムの場合、各メトリクスを慎重に読み解いていく必要があります。
ちなみに、CloudWatchのメトリクスは、各生データの平均(average)、合計(sum)、最大値(max)、最小値(min)等を指定して集計値を見ますが、この集計方法はデータの性質に応じて、各自が設定する必要があります。例えば、CPU使用率というメトリクスは合計(sum)値を見ても意味がわかりません。一般的には平均(average)を見るべきです。ただし、各自の設定次第では「CPU使用率の合計」を表示してしまいます。このように、CloudWatch自身は「このデータはどのように見るべきか」について関知しません。下記でご紹介する各メトリクスは、特に明示しない限り「合計(sum)」として確認するのが適切です。
ELBがバックエンドのステータスコードをそのまま返す場合(HTTPCode_Backend_*)
ELBはそのバックエンドにEC2インスタンスを複数台配置し、主にHTTPのリクエストを各EC2に分散する、まさにロードバランサーとしての役割を果たします。クライアントからELBに届いたHTTPリクエストは、基本的にはバックエンドのEC2に転送され、そのEC2が返したレスポンスをそのままクライアントに返します *2。
このように、バックエンドのステータスコードに従って返したレスポンスの数を以下のようにカウントしています。
HTTPCode_Backend_2XX
(3)及び(4)のレスポンスで200系のHTTPレスポンスを行い、(1)〜(4)までのステップを全て正常に完了した場合、HTTPCode_Backend_2XXとしてカウントされます。一般的に最も健全な動作と言えるでしょう。
HTTPCode_Backend_4XX
(1)〜(4)までのステップを全て正常に完了したのだが、3のレスポンスが400系レスポンスであり、その結果(4)のレスポンスが400系として完了した場合、これはHTTPCode_Backend_4XXとしてカウントされます。存在しないリソースにアクセスして404がとなった場合や、Web APIに於いて必須のパラメータが足りなかった場合の400レスポンス等、クライアント側の「リクエストの仕方」に問題がある場合、400系のレスポンスを返すのが一般的ですね。
HTTPCode_Backend_5XX
(1)〜(4)までのステップを全て正常に完了したのだが、(3)のレスポンスが500系レスポンスであり、その結果(4)のレスポンスが500系として完了した場合、これはHTTPCode_Backend_5XXとしてカウントされます。アプリケーション層でのエラーにより正常なレスポンスが返せなかった場合の500、メンテナンス中の503等、エラーの原因がサーバ側にある場合、500系のレスポンスを返すのが一般的です。
ELBが独自の判断によりステータスコードを返す場合(HTTPCode_ELB_*)
以上の通り、普通は「バックエンドから受け取ったレスポンス(3)をそのままクライアントに返す(4)」のがELBの動きです。しかし、特定の状況下ではELBは独自の判断で(4)のレスポンスを行います。
HTTPCode_ELB_4XX
HTTP 400: BAD_REQUEST
そもそも(1)のリクエストが日本語でおkHTTPリクエストとして解釈不能な場合、ELBは(2)のリクエスト転送を行わず(行えず?)、400を返します。
HTTP 405: METHOD_NOT_ALLOWED
(1)のリクエストにおけるHTTPメソッド名(GETやPOST等)が127文字を超えていた場合、ELBは(2)のリクエスト転送を行わず、405を返します。HTTPメソッドは、よく知られたGET, POST, OPTIONS等の他に、独自の拡張が可能です。(RFC 2616 Section 5.1.1参照)
このような拡張を利用していた時、無駄に長いHTTPメソッド名が使われる可能性もゼロではありません。HTTPの仕様上、この長さには制限は無いようですが、ELBとしては127文字を上限としているようです。
HTTP 408: Request Timeout
(1)のリクエストでタイムアウトが発生した場合、ELBは(2)のリクエスト転送を行わず(行えず?)、408を返します。
例えば、ネットワーク切断が発生した場合や、Content-Lengthの値よりも実際のサイズが小さくてELBが続きを待ってしまう場合など、ELBが(1)の完了を認識できずにタイムアウトした場合があてはまります。
HTTPCode_ELB_5XX
HTTP 502: Bad Gateway
(3)のレスポンスが日本語でおkHTTPレスポンスとして解釈不能な場合、ELBは502を返します。
HTTP 503: Service Unavailable
このエラーは、いくつかの可能性が考えられます。
Case 1: 突発的なアクセスによりELB自体がキャパシティを超えてしまった場合です。ELBは負荷に応じて自動スケールします。しかし、ELBを突発的な負荷が襲った場合、スケーリングが間に合わないことがあります。
また、負荷により「スケールアップ」が行われた場合、ELBのDNS名から正引きされるIPアドレスが変わります。つまり、古い(小さな)インスタンスと、新しい(大きな)インスタンスにバトンタッチが起こります。しかし、クライアントがDNSキャッシュを持っていた場合、スケールアップ前の古いインスタンスに対してリクエストを送り続けてしまうことがあります。こういった場合にも、キャパ超えのレスポンスとして503が返されるでしょう。
このようなケースに対しては、ベンチマークツール等を利用して本番想定の負荷をあらかじめ掛けておき、ELBの自動的なスケーリングによる調整を期待した上で本番運用を開始する、といった対応が必要です。このような対応が行えない場合、AWSサポートのビジネスまたはエンタープライズに加入しているのであれば、暖気申請を行うことも可能です。AWSサポートに対して、いつ頃どの程度のリクエストが来るのか、事前に通知を行うことにより、リクエストを充分受け付けられる規模にあらかじめスケールをしてもらえます。
Case 2: (3)を行った後(4)が返ってくるのに時間が掛かり、(3)のリクエストがタイムアウトした場合です。このタイムアウト時間は通常60秒です。
このケースに対応する場合、アプリケーションが60秒以内にレスポンスを返すように何らかの対応を行うのが第一選択ですが、サポートに依頼することによりタイムアウト時間を最大17分まで延長することができるようです。
2015/05/20 update: 上記は、執筆時当時の情報です。現在は1~3600秒まで設定可能になっています。下記ドキュメントを御覧ください。
http://docs.aws.amazon.com/ja_jp/ElasticLoadBalancing/latest/DeveloperGuide/config-idle-timeout.html
Case 3: ELB配下の各AZにインスタンスが登録されていない場合です。「ロードバランシング戦略」のセクションで解説した通り、ELBでは利用する各AZ毎に1つ以上のインスタンスが必要です。AZ1とAZ2、2つのAZを利用した場合、それぞれについて1つ以上のインスタンス登録が必要です。例えばAZ1に2インスタンスの登録あった場合でも、AZ2にインスタンス登録がなければ、一定の確率で503となる可能性があります。
Case 4: ELB配下に正常(healthy)なインスタンスが1つも無い場合です。インスタンスの登録があっても正常(healthy)でなければ503となります。こちらもCase 3と同様、各AZに正常インスタンスが1つ以上必要です。
その他のメトリクス: RequestCount
以上のように状況に応じて、HTTPCode_Backend_2XX〜HTTPCode_ELB_5XXの各メトリクスがカウントされますが、このうちHTTPCode_Backend_*の合計がRequestCountです。
しばしば勘違いしがちなのは「clientからELBに行われたリクエストの数の合計」つまり(1)の合計数という解釈ですが、違います。この値は、バックエンドのインスタンスにリクエストが転送され、きちんと処理が完了したリクエストの数を表しています。
その他のメトリクス: SurgeQueueLength
ELBは、バックエンドインスタンスがリクエストの処理能力を超えた場合に備えて、surge queueという待ち行列を備えています。例えば、EC2インスタンス上のApache httpdのMaxClientsが飽和し、新たなコネクションを受け付けられない時、ELBはリクエストを一旦このキューに格納し、バッファリングを行います。
このキューの容量は1024で、それ以上のリクエストが来た場合は、バックエンドにリクエストのルーティングは行わず(行えず)、HTTPCode_ELB_5XXとしてレスポンスを行います。
このメトリクスは、このキューにたまったリクエストの数を表します。また、一般的には平均(average)や最大値(max)による集計が適切です。突発的なリクエスト増がなければ、このメトリクスは0を維持します。
その他のメトリクス: SpilloverCount
上記のsurge queueがあふれ、HTTPCode_ELB_5XXが返った数を表します。この分のリクエストは、前述の通りRequestCountには含まれず、サーバサイドにもログが残らないので、気をつけておかなければ気付けない障害となりがちです。
まとめ
身近なELBですが、細かくみていくと注目すべきポイントが多数あることがわかると思います。特にスパイクアクセスを受けるようなシステムを構築する場合や、負荷試験を行う場合、ELBの仕組みと各メトリクスを慎重に観察する必要があるでしょう。
脚注
- 最近リリースされた、Cross-Zone Routingを設定した場合は、その限りではありません。 cf. AWS ELBの社内向け構成ガイドを公開してみる 負荷分散編 – Cross-Zone Routingを踏まえて ↩
- プロキシとして必要となる若干のリクエスト/レスポンスの書き換えは行います。 ↩